퍼플심_04_아키텍쳐 정리하기
들어가는 말
MVP 기능 구현은 어느 정도 일단락되었다.
이번에는 이 시스템이 어떻게 돌아가고 있는지 정리해보려 한다.
나중에 유지보수할 '미래의 나'를 위한 기술 문서이기도 하다.
1. Core Stack:"No Database"
DB 없는 100% 클라이언트 사이드 앱이다.
- Framework: Next.js(App Router)
- Language: TypeScript(Strict Mode)
- State: Zustand
- Storage: LocalStorage(진행 상황 저장용, 예정)
교육용 시뮬레이터 특성상, 사용자마다 독립된 인스턴스가 필요하고 복잡한 백엔드 로직보다는 '인터랙션'이 중요하다고 판단했다.
그래서 무거운 백엔드 대신, 브라우저 메모리 안에서 거대한 상태 머신(State Machine)이 돌아가는 구조를 택했다.
물론 시나리오가 많아지고, 상태가 복잡해지면 이 구조도 문제가 될 수 있다.
이 점은 염두하고 있으며, 나중에 필요하다면 백엔드로 이전할 수 있도록 설계할 것이다.
2. State Management:Zustand Engine
이 앱의 심장은 useScenarioStore다.
단순히 "지금 몇 단계인가?"만 저장하는 게 아니라, 시뮬레이션의 모든 상태를 관리하는 엔진으로 구성했다.
interface ScenarioState {
// Game State
currentRole: 'RED' | 'BLUE';
currentPhaseIndex: number;
// Blue Team Progress (Complex Object)
blueProgress: {
detectedArtifacts: Artifact[]; // 발견한 증거
executedActions: string[]; // 수행한 조치
blockedIPs: string[]; // 차단한 IP
// ...
};
}
레드팀은 선형적(Linear)이라 currentPhaseIndex만 올리면 되지만,
대응을 해야 하는 블루팀은 비선형적이라 blueProgress 객체 안에 수행한 행동들을 누적해서 관리한다.
이 모든 게 Zustand 스토어 하나에서 관리되므로, 컴포넌트 간 데이터 동기화 걱정이 없다.
추후 시나리오의 레드팀 작업 방식에 따라 비선형적으로 다시 설계해야할 수 있다.
3. Scenario as Code
시나리오 데이터(react2shell.ts)는 JSON이 아니라 TypeScript 파일로 관리한다.
타입 안정성(Type Safety)을 위해서 이를 사용한다고 하길래, 한 번 써보기로 했다.
시나리오 흐름이 복잡해질수록 데이터 오타 하나가 치명적인 버그가 되니까.
export const react2shell: Scenario = {
id: "react2shell",
title: "React -> Shell",
phases: [
{
role: "RED",
type: "RECONNAISSANCE",
// ...
},
{
role: "BLUE",
// ...
}
]
};
이렇게 코드로 관리하면 컴파일 타임에 에러를 잡을 수 있고, IDE의 자동 완성 지원도 받을 수 있다.
나중에 시나리오가 100개가 되어도 이 구조라면 관리가 쉬울 것이다.
4. Role Separation(One Store, Two Views)
하나의 스토어를 쓰지만, 뷰(View)는 철저히 분리했다.
- RedTeamWorkbench: 공격자용 UI (터미널, 에디터 중심)
- BlueTeamWorkbench: 방어자용 UI (대시보드, 분석 도구 중심)
currentRole 상태값에 따라 아예 렌더링되는 최상위 컴포넌트가 갈아끼워지는 방식이다.
덕분에 코드가 섞이지 않고, 각 역할에 특화된 UI/UX를 마음껏 구현할 수 있었다.
홈 랜딩 페이지도 좀 꾸몄다.
